<?php
/***********************************************************************
 * @package    IvanEngine
 * @subpackage Core
 * @author     Ivan <ivanzx@msn.com>
 * @created    2010-05-23
 ***********************************************************************/
if(defined("IVANENGINE_CORE_RESPONSE"))
    die("Redefinition macro in :".__FILE__."(".__LINE__.")");
define("IVANENGINE_CORE_RESPONSE", true);

require_once (dirname(__FILE__).'/Common.php');

class CResponse extends CEventSingleton
{
	const   OT_Charset           = 'charset',
            OT_SendHttpHeaders   = 'Send_http_headers',
			OT_HttpProtocol      = 'http_protocol',
			OT_ContentType       = 'Content_type',
            
		    FIRST                = 'first',
		    MIDDLE               = '',
		    LAST                 = 'last',
		    ALL                  = 'ALL',
		    RAW                  = 'RAW';
	
	
    protected
        $m_options     = array(),
        $m_content     = '',
        $m_cookies     = array(),
        $m_statusCode  = 200,
        $m_statusText  = 'OK',
        $m_headerOnly  = false,
        $m_headers     = array(),
        $m_metas       = array(),
        $m_httpMetas   = array(),
        $m_positions   = array('first', '', 'last'),
        $m_stylesheets = array(),
        $m_javascripts = array(),
        $m_slots       = array();
        
    static protected $m_statusTexts = array(
        '100' => 'Continue',
        '101' => 'Switching Protocols',
        '200' => 'OK',
        '201' => 'Created',
        '202' => 'Accepted',
        '203' => 'Non-Authoritative Information',
        '204' => 'No Content',
        '205' => 'Reset Content',
        '206' => 'Partial Content',
        '300' => 'Multiple Choices',
        '301' => 'Moved Permanently',
        '302' => 'Found',
        '303' => 'See Other',
        '304' => 'Not Modified',
        '305' => 'Use Proxy',
        '306' => '(Unused)',
        '307' => 'Temporary Redirect',
        '400' => 'Bad Request',
        '401' => 'Unauthorized',
        '402' => 'Payment Required',
        '403' => 'Forbidden',
        '404' => 'Not Found',
        '405' => 'Method Not Allowed',
        '406' => 'Not Acceptable',
        '407' => 'Proxy Authentication Required',
        '408' => 'Request Timeout',
        '409' => 'Conflict',
        '410' => 'Gone',
        '411' => 'Length Required',
        '412' => 'Precondition Failed',
        '413' => 'Request Entity Too Large',
        '414' => 'Request-URI Too Long',
        '415' => 'Unsupported Media Type',
        '416' => 'Requested Range Not Satisfiable',
        '417' => 'Expectation Failed',
        '500' => 'Internal Server Error',
        '501' => 'Not Implemented',
        '502' => 'Bad Gateway',
        '503' => 'Service Unavailable',
        '504' => 'Gateway Timeout',
        '505' => 'HTTP Version Not Supported',
    );
    static protected $m_this= null;
    
    public function __construct()
    {
        parent::__construct();
    }

    public function __destruct()
    {
        parent::__destruct();
    }
    
    static public function Get()
    {
        if( self::$m_this == null )
        {
            parent::StartSingleton();
            self::$m_this = new CResponse();
            parent::EndSingleton();
        }
        return self::$m_this;
    }
    
   /**
    * Initializes this sfResponse.
    *
    * Available options:
    * @param  array              $options     An array of options
    *
    * @return bool true, if initialization completes successfully, otherwise false
    *
    * @throws <b>CInitializationException</b> If an error occurs while initializing this CResponse
    */
    public function Initialize($options = array())
    {
        $this->m_options     = $options;
        
        $this->m_javascripts = array_combine($this->m_positions, array_fill(0, count($this->m_positions), array()));
        $this->m_stylesheets = array_combine($this->m_positions, array_fill(0, count($this->m_positions), array()));

        if (!isset($this->m_options[self::OT_Charset]))
        {
            $this->m_options[self::OT_Charset] = 'utf-8';
        }
    
        if (!isset($this->m_options[self::OT_SendHttpHeaders]))
        {
            $this->m_options[self::OT_SendHttpHeaders] = true;
        }
    
        if (!isset($this->m_options[self::OT_HttpProtocol]))
        {
            $this->m_options[self::OT_HttpProtocol] = 'HTTP/1.0';
        }
    
        $this->m_options[self::OT_ContentType] = $this->FixContentType(
           isset($this->m_options[self::OT_ContentType]) ? 
           $this->m_options[self::OT_ContentType] : 
           'text/html');
    }

    /**
    * Sets the response content
    *
    * @param string $content
    */
    public function SetContent($content)
    {
        $this->m_content = $content;
    }

    /**
    * Gets the current response content
    *
    * @return string Content
    */
    public function GetContent()
    {
        return $this->m_content;
    }

    /**
    * Outputs the response content
    */
    public function SendContent()
    {
    	if (!$this->m_headerOnly)
        {
	        //$this->FireEvent(self::EVT_SENDCONTENT, new CEventArgs());
	    
	        echo $this->GetContent();
        }
    }

    /**
    * Sends the HTTP headers and the content.
    */
    public function Send()
    {
        $this->SendHttpHeaders();
        $this->SendContent();
    }

    public function GetOptions()
    {
        return $this->m_options;
    }
    
    /**
    * Sets if the response consist of just HTTP headers.
    *
    * @param bool $value
    */
    public function SetHeaderOnly($value = true)
    {
        $this->m_headerOnly = (boolean) $value;
    }

    /**
    * Returns if the response must only consist of HTTP headers.
    *
    * @return bool returns true if, false otherwise
    */
    public function IsHeaderOnly()
    {
        return $this->m_headerOnly;
    }

    /**
    * Sets a cookie.
    *
    * @param  string  $name      HTTP header name
    * @param  string  $value     Value for the cookie
    * @param  string  $expire    Cookie expiration period
    * @param  string  $path      Path
    * @param  string  $domain    Domain name
    * @param  bool    $secure    If secure
    * @param  bool    $httpOnly  If uses only HTTP
    *
    * @throws <b>CException</b> If fails to set the cookie
    */
    public function SetCookie($name, $value, $expire = null, $path = '/', $domain = '', $secure = false, $httpOnly = false)
    {
        if ($expire !== null)
        {
            if (is_numeric($expire))
            {
                $expire = (int) $expire;
            }
            else
            {
                $expire = strtotime($expire);
                if ($expire === false || $expire == -1)
                {
                    throw new CException('Your expire parameter is not valid.');
                }
            }
        }

        $this->m_cookies[$name] = array(
          'name'     => $name,
          'value'    => $value,
          'expire'   => $expire,
          'path'     => $path,
          'domain'   => $domain,
          'secure'   => $secure ? true : false,
          'httpOnly' => $httpOnly,
        );
    }

    /**
    * Sets response status code.
    *
    * @param string $code  HTTP status code
    * @param string $name  HTTP status text
    *
    */
    public function SetStatusCode($code, $name = null)
    {
        $this->m_statusCode = $code;
        $this->m_statusText = null !== $name ? $name : self::$m_statusTexts[$code];
    }

    /**
    * Retrieves status text for the current web response.
    *
    * @return string Status text
    */
    public function GetStatusText()
    {
        return $this->m_statusText;
    }

    /**
    * Retrieves status code for the current web response.
    *
    * @return integer Status code
    */
    public function GetStatusCode()
    {
        return $this->m_statusCode;
    }

    /**
    * Sets a HTTP header.
    *
    * @param string  $name     HTTP header name
    * @param string  $value    Value (if null, remove the HTTP header)
    * @param bool    $replace  Replace for the value
    *
    */
    public function SetHttpHeader($name, $value, $replace = true)
    {
        $name = $this->_NormalizeHeaderName($name);

        if (is_null($value))
        {
            unset($this->m_headers[$name]);

            return;
        }

        if ('Content-Type' == $name)
        {
            if ($replace || !$this->GetHttpHeader('Content-Type', null))
            {
                $this->SetContentType($value);
            }

            return;
        }

        if (!$replace)
        {
            $current = isset($this->m_headers[$name]) ? $this->m_headers[$name] : '';
            $value = ($current ? $current.', ' : '').$value;
        }

        $this->m_headers[$name] = $value;
    }

    /**
    * Gets HTTP header current value.
    *
    * @param  string $name     HTTP header name
    * @param  string $default  Default value returned if named HTTP header is not found
    *
    * @return array
    */
    public function GetHttpHeader($name, $default = null)
    {
        $name = $this->_NormalizeHeaderName($name);

        return isset($this->m_headers[$name]) ? $this->m_headers[$name] : $default;
    }

    /**
    * Checks if response has given HTTP header.
    *
    * @param  string $name  HTTP header name
    *
    * @return bool
    */
    public function HasHttpHeader($name)
    {
        return array_key_exists($this->_NormalizeHeaderName($name), $this->m_headers);
    }

    /**
    * Sets response content type.
    *
    * @param string $value  Content type
    *
    */
    public function SetContentType($value)
    {
        $this->m_headers['Content-Type'] = $this->FixContentType($value);
    }

    /**
    * Gets the current charset as defined by the content type.
    *
    * @return string The current charset
    */
    public function GetCharset()
    {
        return $this->m_options[self::OT_Charset];
    }

    /**
    * Gets response content type.
    *
    * @return array
    */
    public function GetContentType()
    {
        return $this->GetHttpHeader('Content-Type', $this->m_options[self::OT_ContentType]);
    }

    /**
    * Sends HTTP headers and cookies.
    *
    */
    public function SendHttpHeaders()
    {
        if (!$this->m_options[self::OT_SendHttpHeaders])
        {
            return;
        }

        // status
        $status = $this->m_options[self::OT_HttpProtocol].' '.$this->m_statusCode.' '.$this->m_statusText;
        header($status);

        // headers
        if (!$this->GetHttpHeader(self::OT_ContentType))
        {
            $this->SetContentType($this->m_options[self::OT_ContentType]);
        }
        foreach ($this->m_headers as $name => $value)
        {
            header($name.': '.$value);

            if ($value != '')
            {
                CLog::Get()->Note(sprintf('Send header "%s: %s"', $name, $value));
            }
        }

        // cookies
        foreach ($this->m_cookies as $cookie)
        {
            setrawcookie($cookie['name'], $cookie['value'], $cookie['expire'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httpOnly']);
            CLog::Get()->Note(sprintf('Send cookie "%s": "%s"', $cookie['name'], $cookie['value']));
        }
    }

    /**
    * Retrieves a normalized Header.
    *
    * @param  string $name  Header name
    *
    * @return string Normalized header
    */
    protected function _NormalizeHeaderName($name)
    {
        return preg_replace('/\-(.)/e', "'-'.strtoupper('\\1')", strtr(ucfirst(strtolower($name)), '_', '-'));
    }

    /**
    * Retrieves a formated date.
    *
    * @param  string $timestamp  Timestamp
    * @param  string $type       Format type
    *
    * @return string Formatted date
    */
    static public function GetDate($timestamp, $type = 'rfc1123')
    {
        $type = strtolower($type);

        if ($type == 'rfc1123')
        {
            return substr(gmdate('r', $timestamp), 0, -5).'GMT';
        }
        else if ($type == 'rfc1036')
        {
            return gmdate('l, d-M-y H:i:s ', $timestamp).'GMT';
        }
        else if ($type == 'asctime')
        {
            return gmdate('D M j H:i:s', $timestamp);
        }
        else
        {
            throw new CExceptionInvalidArgument('The second GetDate() method parameter must be one of: rfc1123, rfc1036 or asctime.');
        }
    }

    /**
    * Adds vary to a http header.
    *
    * @param string $header  HTTP header
    */
    public function AddVaryHttpHeader($header)
    {
        $vary = $this->GetHttpHeader('Vary');
        $currentHeaders = array();
        if ($vary)
        {
            $currentHeaders = preg_split('/\s*,\s*/', $vary);
        }
        $header = $this->_NormalizeHeaderName($header);

        if (!in_array($header, $currentHeaders))
        {
            $currentHeaders[] = $header;
            $this->SetHttpHeader('Vary', implode(', ', $currentHeaders));
        }
    }

    /**
    * Adds an control cache http header.
    *
    * @param string $name   HTTP header
    * @param string $value  Value for the http header
    */
    public function AddCacheControlHttpHeader($name, $value = null)
    {
        $cacheControl = $this->GetHttpHeader('Cache-Control');
        $currentHeaders = array();
        if ($cacheControl)
        {
            foreach (preg_split('/\s*,\s*/', $cacheControl) as $tmp)
            {
                $tmp = explode('=', $tmp);
                $currentHeaders[$tmp[0]] = isset($tmp[1]) ? $tmp[1] : null;
            }
        }
        $currentHeaders[strtr(strtolower($name), '_', '-')] = $value;

        $headers = array();
        foreach ($currentHeaders as $key => $value)
        {
            $headers[] = $key.(null !== $value ? '='.$value : '');
        }

        $this->SetHttpHeader('Cache-Control', implode(', ', $headers));
    }

    /**
    * Retrieves meta headers for the current web response.
    *
    * @return string Meta headers
    */
    public function GetHttpMetas()
    {
        return $this->m_httpMetas;
    }

    /**
    * Adds a HTTP meta header.
    *
    * @param string  $key      Key to replace
    * @param string  $value    HTTP meta header value (if null, remove the HTTP meta)
    * @param bool    $replace  Replace or not
    */
    public function AddHttpMeta($key, $value, $replace = true)
    {
        $key = $this->_NormalizeHeaderName($key);

        // set HTTP header
        $this->SetHttpHeader($key, $value, $replace);

        if (is_null($value))
        {
            unset($this->m_httpMetas[$key]);

            return;
        }

        if ('Content-Type' == $key)
        {
            $value = $this->GetContentType();
        }
        elseif (!$replace)
        {
            $current = isset($this->m_httpMetas[$key]) ? $this->m_httpMetas[$key] : '';
            $value = ($current ? $current.', ' : '').$value;
        }

        $this->m_httpMetas[$key] = $value;
    }

    /**
    * Retrieves all meta headers.
    *
    * @return array List of meta headers
    */
    public function GetMetas()
    {
        return $this->m_metas;
    }

    /**
    * Adds a meta header.
    *
    * @param string  $key      Name of the header
    * @param string  $value    Meta header value (if null, remove the meta)
    * @param bool    $replace  true if it's replaceable
    * @param bool    $escape   true for escaping the header
    */
    public function AddMeta($key, $value, $replace = true, $escape = true)
    {
        $key = strtolower($key);

        if (is_null($value))
        {
            unset($this->m_metas[$key]);

            return;
        }

        // FIXME: If you use the i18n layer and escape the data here, it won't work
        // see include_metas() in AssetHelper
        if ($escape)
        {
            $value = htmlspecialchars($value, ENT_QUOTES, $this->m_options[self::OT_Charset]);
        }

        $current = isset($this->m_metas[$key]) ? $this->m_metas[$key] : null;
        if ($replace || !$current)
        {
            $this->m_metas[$key] = $value;
        }
    }

    /**
    * Retrieves title for the current web response.
    *
    * @return string Title
    */
    public function GetTitle()
    {
        return isset($this->m_metas['title']) ? $this->m_metas['title'] : '';
    }

    /**
    * Sets title for the current web response.
    *
    * @param string  $title   Title name
    * @param bool    $escape  true, for escaping the title
    */
    public function SetTitle($title, $escape = true)
    {
        $this->AddMeta('title', $title, true, $escape);
    }

    /**
    * Returns the available position names for stylesheets and javascripts in order.
    *
    * @return array An array of position names
    */
    public function GetPositions()
    {
        return $this->m_positions;
    }

    /**
    * Retrieves stylesheets for the current web response.
    *
    * By default, the position is CResponse::ALL,
    * and the method returns all stylesheets ordered by position.
    *
    * @param  string  $position The position
    *
    * @return array   An associative array of stylesheet files as keys and options as values
    */
    public function GetStylesheets($position = self::ALL)
    {
        if (self::ALL === $position)
        {
            $stylesheets = array();
            foreach ($this->GetPositions() as $position)
            {
                foreach ($this->m_stylesheets[$position] as $file => $options)
                {
                    $stylesheets[$file] = $options;
                }
            }

            return $stylesheets;
        }
        else if (self::RAW === $position)
        {
            return $this->m_stylesheets;
        }

        $this->_ValidatePosition($position);

        return $this->m_stylesheets[$position];
    }

    /**
    * Adds a stylesheet to the current web response.
    *
    * @param string $file      The stylesheet file
    * @param string $position  Position
    * @param string $options   Stylesheet options
    */
    public function AddStylesheet($file, $position = '', $options = array())
    {
        $this->_ValidatePosition($position);

        $this->m_stylesheets[$position][$file] = $options;
    }

    /**
    * Removes a stylesheet from the current web response.
    *
    * @param string $file The stylesheet file to remove
    */
    public function RemoveStylesheet($file)
    {
        foreach ($this->GetPositions() as $position)
        {
            unset($this->m_stylesheets[$position][$file]);
        }
    }

    /**
    * Retrieves javascript files from the current web response.
    *
    * By default, the position is CResponse::ALL,
    * and the method returns all javascripts ordered by position.
    *
    * @param  string $position  The position
    *
    * @return array An associative array of javascript files as keys and options as values
    */
    public function GetJavascripts($position = self::ALL)
    {
        if (self::ALL === $position)
        {
            $javascripts = array();
            foreach ($this->GetPositions() as $position)
            {
                foreach ($this->m_javascripts[$position] as $file => $options)
                {
                    $javascripts[$file] = $options;
                }
            }

            return $javascripts;
        }
        else if (self::RAW === $position)
        {
            return $this->m_javascripts;
        }

        $this->_ValidatePosition($position);

        return $this->m_javascripts[$position];
    }

    /**
    * Adds javascript code to the current web response.
    *
    * @param string $file      The JavaScript file
    * @param string $position  Position
    * @param string $options   Javascript options
    */
    public function AddJavascript($file, $position = '', $options = array())
    {
        $this->_ValidatePosition($position);

        $this->m_javascripts[$position][$file] = $options;
    }

    /**
    * Removes a JavaScript file from the current web response.
    *
    * @param string $file The Javascript file to remove
    */
    public function RemoveJavascript($file)
    {
        foreach ($this->GetPositions() as $position)
        {
            unset($this->m_javascripts[$position][$file]);
        }
    }

    /**
    * Retrieves slots from the current web response.
    *
    * @return string Javascript code
    */
    public function GetSlots()
    {
        return $this->m_slots;
    }

    /**
    * Sets a slot content.
    *
    * @param string $name     Slot name
    * @param string $content  Content
    */
    public function SetSlot($name, $content)
    {
        $this->m_slots[$name] = $content;
    }

    /**
    * Retrieves cookies from the current web response.
    *
    * @return array Cookies
    */
    public function GetCookies()
    {
        return $this->m_cookies;
    }

    /**
    * Retrieves HTTP headers from the current web response.
    *
    * @return string HTTP headers
    */
    public function GetHttpHeaders()
    {
        return $this->m_headers;
    }

    /**
    * Cleans HTTP headers from the current web response.
    */
    public function ClearHttpHeaders()
    {
        $this->m_headers = array();
    }

    /**
    * Copies all properties from a given CResponse object to the current one.
    *
    * @param CResponse $response  An CResponse instance
    */
    public function CopyProperties(CResponse $response)
    {
        $this->m_options     = $response->getOptions();
        $this->m_headers     = $response->GetHttpHeaders();
        $this->m_metas       = $response->GetMetas();
        $this->m_httpMetas   = $response->GetHttpMetas();
        $this->m_stylesheets = $response->GetStylesheets(self::RAW);
        $this->m_javascripts = $response->GetJavascripts(self::RAW);
        $this->m_slots       = $response->GetSlots();
    }

    /**
    * Merges all properties from a given CResponse object to the current one.
    *
    * @param CResponse $response  An CResponse instance
    */
    public function Merge(CResponse $response)
    {
        foreach ($this->GetPositions() as $position)
        {
            $this->m_javascripts[$position] = array_Merge($this->GetJavascripts($position), $response->GetJavascripts($position));
            $this->m_stylesheets[$position] = array_Merge($this->GetStylesheets($position), $response->GetStylesheets($position));
        }

        $this->m_slots = array_Merge($this->GetSlots(), $response->GetSlots());
    }

    /**
    * @see sfResponse
    */
    public function serialize()
    {
        return serialize(array($this->m_content, $this->m_statusCode, $this->m_statusText, $this->m_options, $this->m_cookies, $this->m_headerOnly, $this->m_headers, $this->m_metas, $this->m_httpMetas, $this->m_stylesheets, $this->m_javascripts, $this->m_slots));
    }

    /**
    * @see sfResponse
    */
    public function unserialize($serialized)
    {
        list($this->m_content, $this->m_statusCode, $this->m_statusText, $this->m_options, $this->m_cookies, $this->m_headerOnly, $this->m_headers, $this->m_metas, $this->m_httpMetas, $this->m_stylesheets, $this->m_javascripts, $this->m_slots) = unserialize($serialized);
    }

    /**
    * Validate a position name.
    *
    * @param  string $position
    *
    * @throws CExceptionInvalidArgument if the position is not available
    */
    protected function _ValidatePosition($position)
    {
        if (!in_array($position, $this->m_positions, true))
        {
            throw new CExceptionInvalidArgument(sprintf('The position "%s" does not exist (available positions: %s).', $position, implode(', ', $this->m_positions)));
        }
    }

    /**
    * Fixes the content type by adding the charset for text content types.
    *
    * @param  string $contentType  The content type
    *
    * @return string The content type with the charset if needed
    */
    protected function FixContentType($contentType)
    {
        // add charset if needed (only on text content)
        if (false === stripos($contentType, 'charset') && (0 === stripos($contentType, 'text/') || strlen($contentType) - 3 === strripos($contentType, 'xml')))
        {
            $contentType .= '; charset='.$this->m_options[self::OT_Charset];
        }

        // change the charset for the response
        if (preg_match('/charset\s*=\s*(.+)\s*$/', $contentType, $match))
        {
            $this->m_options[self::OT_Charset] = $match[1];
        }

        return $contentType;
    }
}
?>